﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using MyBasicLib.Collections.Generic;
using MyBasicLib.Extensions;
using MyBasicLib.Utilities.Log;
using MyBasicLib.Utilities;

namespace MyBasicLib.Data
{
    public abstract class Database : IDatabase
    {
        ILogger logger;
        public ILogger Logger
        {
            get { return logger ?? (logger = IOC.DefaultContainer.Resolve<ILogger>()); }
            set { logger = value; }
        }

        protected Database(DbProviderFactory providerFactory, string connectionString)
        {
            DbProviderFactory = providerFactory;
            ConnectionString = connectionString;
        }

        protected abstract int UserParametersStartIndex { get; }


        public DbProviderFactory DbProviderFactory { get; private set; }

        public string ConnectionString { get; set; }
        public abstract string ParameterToken { get; }

        public IDbConnection CreateConnection()
        {
            DbConnection conn = DbProviderFactory.CreateConnection();
            conn.ConnectionString = ConnectionString;
            return conn;
        }
        public IDbConnection CreateNewOpenConnection()
        {
            IDbConnection connection = null;

            try
            {
                connection = CreateConnection();
                connection.Open();
            }
            catch (Exception e)
            {
                if (connection != null)
                    connection.Close();
                if (Logger != null) Logger.Error(e.ToString());

                throw;
            }
            return connection;
        }

        public IDbCommand CreateCommand(string commandText)
        {
            var cmd = DbProviderFactory.CreateCommand();
            cmd.CommandText = commandText;
            cmd.CommandTimeout = 60;
            return cmd;
        }

        public T Execute<T>(IDbTransaction transaction, IDbCommand cmd)
        {
            using (IDataReader reader = ExecuteReader(transaction, cmd))
            {
                if (reader.Read())
                {
                    return reader.ToModel<T>();
                }
            }
            return default(T);
        }

        public IEnumerable<T> ExecuteList<T>(IDbTransaction transaction, IDbCommand cmd)
        {

            using (IDataReader reader = ExecuteReader(transaction, cmd))
            {
                while (reader.Read())
                {
                    yield return reader.ToModel<T>();//return default(T); //
                }
            }
        }

        public int ExecuteNonQuery(IDbTransaction transaction, IDbCommand cmd)
        {
            return transaction == null
                       ? DoExecute(cmd, p => p.ExecuteNonQuery(), true)
                       : DoExecute(transaction, cmd, p => p.ExecuteNonQuery());
        }

        public IDbCommand GetStoredProcedure(string storedProcedureName, params object[] parameterValues)
        {
            IDbCommand cmd = CreateCommand(storedProcedureName);
            cmd.CommandType = CommandType.StoredProcedure;
            var pa = cmd.CreateParameter();
            pa.ParameterName = "@ReturnValue";
            pa.Direction = ParameterDirection.ReturnValue;
            pa.DbType = DbType.Int32;
            cmd.Parameters.Add(pa);

            AssignParameterValues((DbCommand)cmd, parameterValues);
            return cmd;
        }

        private void AssignParameterValues(DbCommand command,
                                           object[] values)
        {
            foreach (var pValue in values)
            {
                var parameter = command.CreateParameter();
                parameter.Value = pValue;
                var size = 0;
                var parString = parameter.Value as string;
                if (parString != null)
                {
                    if (parString.Length <= 36)
                        size = 36;
                    else if (parString.Length <= 100)
                        size = 100;
                    else if (parString.Length <= 1000)
                        size = 1000;
                    else if (parString.Length <= 4000)
                        size = 4000;
                }
                parameter.Size = size;
                command.Parameters.Add(parameter);
            }
        }

        public IDataReader ExecuteReader(IDbTransaction transaction, IDbCommand cmd)
        {
            return transaction == null
                       ? DoExecute(cmd, p => p.ExecuteReader(CommandBehavior.CloseConnection), false)
                       : DoExecute(transaction, cmd, p => p.ExecuteReader());
        }


        public object ExecuteScalar(IDbTransaction transaction, IDbCommand cmd)
        {
            return transaction == null
                       ? DoExecute(cmd, p => p.ExecuteScalar(), true)
                       : DoExecute(transaction, cmd, p => p.ExecuteScalar());
        }

        public object ExecuteScalar(IDbTransaction transaction, string storedProcedureName,
                                    params object[] parameterValues)
        {
            return ExecuteScalar(transaction, GetStoredProcedure(storedProcedureName, parameterValues));
        }



        public T Execute<T>(IDbTransaction transaction, IDbCommand cmd, T t)
        {
            return Execute<T>(transaction, cmd);
        }

        public IEnumerable<T> ExecuteList<T>(IDbTransaction transaction, IDbCommand cmd, T t)
        {
            return ExecuteList<T>(transaction, cmd);
        }
        protected T DoExecute<T>(IDbTransaction transaction, IDbCommand cmd, Func<IDbCommand, T> func)
        {
            cmd.Transaction = transaction;
            cmd.Connection = transaction.Connection;
            if (Logger != null) Logger.Info(() => cmd.CommandText);

            return func(cmd);
        }

        protected virtual T DoExecute<T>(IDbCommand cmd, Func<IDbCommand, T> func, bool closeConnection)
        {
            if (Logger != null) Logger.Info(() => cmd.CommandText);

            using (cmd)
            {
                T obj;

                IDbConnection conn = CreateNewOpenConnection();
                cmd.Connection = conn;

                try
                {
                    obj = func(cmd);
                    if (closeConnection)
                    {
                        conn.Close();
                    }
                }
                catch (Exception ex)
                {
                    if (conn != null)
                        conn.Close();
                    if (Logger != null) Logger.Error(cmd.CommandText + Environment.NewLine + ex);
                    throw;
                }
                return obj;
            }
        }

    }
}